'''
* 文件名称: Build.py
* 内容摘要: KOS 工程管理
* 创建者の: 孔佳伟
* 个人主页: https://gitee.com/thin-wind/jump
* 修改记录: 
'''

import os
import struct
import json

OUTPUT      = "output"
IMG         = "a.img"

BOOT_SRC    = "boot.asm"
BOOT_BIN    = "boot.bin"

LOADER_SRC  = "loader.asm"
LOADER_BIN  = "loader.bin"

KENTRY_SRC  = "kentry.asm"
KENTRY_O    = "kentry.o"

KERNEL_OUT  = "kernel.out"
KERNEL_BIN  = "kernel.bin"

APP_DIR     = "app"
APP_OUT     = "output/app"
APP_BIN     = "app.bin"

USER_DIR    = "user"

ROOT = os.getcwd()          # 获取当前所在路径的绝对路径作为工程根目录
BUILD_CFG = "BUILD.json"    # 编译配置文件名
project = {}                # 通过递归遍历编译配置文件 BUILD_CFG 后所得信息放入 project 字典中

# 该函数通过递归遍历路径 path 下的所有的 BUILD.json 文件，
# 并将遍历后的结果以 '源文件文件名':['源文件所在路径', ['源文件所包含的头文件路径1', '源文件所包含的头文件路径1']] 
# 这种形式的数据添加到字典 project 中
def Parse_BUILD_CFG(path):
    JsonPathName = os.path.join(path, BUILD_CFG)
    # 以只读方式打开文件 JsonPathName
    with open(JsonPathName, 'r') as f:
        # 读取文件内容到 json_text  
        json_text = f.read()
        # 将 json_text 内容转为 python 字典 json_dict
        json_dict = json.loads(json_text)
        # 遍历字典 json_dict
        for key in json_dict:
            if key == 'src':    # 如果是源文件，则将 '源文件文件名':['源文件所在路径', ['源文件所包含的头文件路径1', '源文件所包含的头文件路径1']] 这种形式的数据添加到字典 project 中
                if json_dict[key]:
                    for item in json_dict[key]:
                        project[item] = [path, json_dict['inc']]
                        
            elif key == 'dir':  # 如果是文件夹，则进入该文件夹内，递归调用 Parse_BUILD_CFG 
                if json_dict[key]:
                    for item in json_dict[key]:
                        new_path = os.path.join(path, item)
                        Parse_BUILD_CFG(new_path)
            elif key == 'inc': # 如果是头文件路径，则不处理
                pass
            
            else:
                print("Invalid key")

    return project

# 编译 boot
def compile_boot(boot_src, boot_bin):
    # 切换到 boot.asm 所在的目录
    cmd_str = os.path.join(ROOT, project[boot_src][0])
    print('Enter: ' + cmd_str)
    os.chdir(cmd_str)
    # 编译 boot.asm
    src =  boot_src
    out = os.path.join(ROOT, OUTPUT, boot_bin)
    cmd_str = "nasm " + src + " -o " + out
    print(cmd_str)
    os.system(cmd_str)

# 编译 loader
def compile_loader(loader_src, loader_bin):
    # 切换到 loader.asm 所在的目录
    cmd_str = os.path.join(ROOT, project[loader_src][0])
    print('Enter: ' + cmd_str)
    os.chdir(cmd_str)
    # 编译 loader.asm
    src = loader_src
    out = os.path.join(ROOT, OUTPUT, loader_bin)
    cmd_str = "nasm " + src + " -o " + out
    print(cmd_str)
    os.system(cmd_str)

# 编译内核
def compile_kernel(kernel_src):
    # 遍历 kernel 源文件
    for key in kernel_src:
        if key[-2:] == '.c':
            # 切换到 .c 源文件所在的目录
            cmd_str = os.path.join(ROOT, kernel_src[key][0])
            print('Enter: ' + cmd_str)
            os.chdir(cmd_str)

            # 获取包含头文件路径
            include = ''
            for item in kernel_src[key][1]:
                tmp = ' -I' + os.path.join(ROOT, item)
                include += tmp
            
            # 编译 .c 文件
            # 例：gcc -m32 -Iinclude -c main.c -o main.o
            src = key
            obj = key[0:-2] + '.o'
            out = os.path.join(ROOT, OUTPUT, obj)
            cmd_str = 'gcc -m32 -nostdinc -fno-stack-protector' + include + " -c " + src + " -o " + out
            print(cmd_str)
            os.system(cmd_str)

        elif key[-4:] == '.asm':
            # boot.asm 和 loader.asm 已经单独处理过了
            if key != 'boot.asm' and key != 'loader.asm':
                # 切换到 .asm 源文件所在的目录
                cmd_str = os.path.join(ROOT, kernel_src[key][0])
                print('Enter: ' + cmd_str)
                os.chdir(cmd_str)

                # 编译 .asm 文件
                src = key
                obj = key[0:-4] + '.o'
                out = os.path.join(ROOT, OUTPUT, obj)
                cmd_str = "nasm -f elf32 " + src + " -o " + out
                print(cmd_str)
                os.system(cmd_str)

        else:
            print(key + " is unknow.")

# 链接 kernel obj 临时文件
def ld_kernel_obj(kernel_out):
    # 遍历 .o 文件
    obj = ''
    for item in os.listdir(os.path.join(ROOT, OUTPUT)):
        # 排除 kentry.o，它是 kernel 程序的入口，链接时需要放在最前面。其它 .o 文件链接时不考虑先后顺序
        if item[-2:] == '.o' and item != 'kentry.o':
            tmp = ' ' + item
            obj += tmp
     # 链接 kernel
    src = KENTRY_O + obj
    out = kernel_out
    cmd_str = "ld -Ttext 0xB000 -s -m elf_i386 -o " + out + " " + src
    print(cmd_str)
    os.system(cmd_str)

# 提取 elf 程序中的 bin 有效数据
def elf_2_bin(elf, bin):
    # 将 kernel.out 转为 kernel.bin
    # objcopy -O binary kernel.out kernel.bin
    src = elf
    out = bin
    cmd_str = "objcopy -O binary " + src + " " + out
    print(cmd_str)
    os.system(cmd_str)

# 生成最终系统镜像
def write_img(boot_bin, loader_bin, kernel_bin, app_bin):
    # 获取 loader.bin 所占扇区数
    stats = os.stat(loader_bin)
    loader_blocks = int(stats.st_size / 512) + 1 

    # 获取 kernel.bin 所占扇区数
    stats = os.stat(kernel_bin)
    kernel_blocks = int(stats.st_size / 512) + 1 

    # 获取 app.bin 所占扇区数
    stats = os.stat(app_bin)
    app_blocks = int(stats.st_size / 512) + 1 

    # 创建一个 60M 的虚拟硬盘，由于搭了两套开发环境，所以这里 bximage 有两种命令格式
    # 使用 “bximage --help” 命令可获得 bximage 的用法
    # 另一个：bximage $@ -func="create" -hd=60 -imgmode="flat" -q
    # 自动计算虚拟硬盘大小，单位 M
    hard_size = int(os.stat(kernel_bin).st_size/1024/1024) + 80
    # cmd_str = "bximage " + IMG + ' -hd -size=' + str(hard_size) + ' -mode="flat" -q'
    cmd_str = "bximage -func=create " + IMG + ' -hd=' + str(hard_size) + ' -imgmode="flat" sectsize=512 -q'
    print(cmd_str)
    os.system(cmd_str)

    # 将 boot.bin 写入硬盘 a.img 的扇区 0
    cmd_str = "dd if=" + boot_bin + " of=" + IMG + " bs=512 count=1 conv=notrunc"
    print(cmd_str)
    os.system(cmd_str)

	# 将 loader.bin 写入硬盘 a.img 的扇区 2
    cmd_str = "dd if=" + loader_bin + " of=" + IMG + " bs=512 count=" + str(loader_blocks) + " seek=2 conv=notrunc"
    print(cmd_str)
    os.system(cmd_str)

	# 将 kernel.bin 写入硬盘 a.img 的扇区 3
    # cmd_str = "dd if=" + KERNEL_BIN + " of=" + IMG + " bs=512 count=" + str(kernel_blocks) + " seek=3 conv=notrunc"
    cmd_str = "dd if=" + kernel_bin + " of=" + IMG + " bs=512 count=" + str(kernel_blocks) + " seek=" + str(2+loader_blocks) + " conv=notrunc"
    print(cmd_str)
    os.system(cmd_str)

    # 将 app.bin 写入硬盘 a.img 中
    cmd_str = "dd if=" + app_bin + " of=" + IMG + " bs=512 count=" + str(app_blocks) + " seek=" + str(2+loader_blocks+kernel_blocks) + " conv=notrunc"
    print(cmd_str)
    os.system(cmd_str)

    # 读取 IMG 文件，将 loader.bin 所占扇区数写到扇区 1 的前两个字节中；
    # 将 kernel.bin 所占扇区数写到扇区 1 的第 3-4 字节中
    # 将 app.bin 所占扇区数写到扇区 1 的第 5-6 字节中
    img = []
    with open(IMG, "rb") as f:
        data = f.read()
        for i in range(len(data)):
            img.append(data[i])
        img[512] = loader_blocks
        img[513] = loader_blocks>>8
        img[514] = kernel_blocks
        img[515] = kernel_blocks>>8
        img[516] = app_blocks
        img[517] = app_blocks>>8
    with open(IMG, 'wb') as f:  
        for x in img:
            f.write(struct.pack('B', x))

# 切换到 user 目录下
# 执行 user 下的 UserBuild.py
# 复制 OUTPUT/user 下的 *.o 到 OUTPUT/app 目录下
# 切换到 app 目录下执行 python AppBuild.py 命令
# 再到 OUTPUT/app 下复制 app.bin 到 OUTPUT 目录下 
def handle_app():
    # 切换到 user 目录下
    cmd_str = os.path.join(ROOT, USER_DIR)
    print('Enter: ' + cmd_str)
    os.chdir(cmd_str)
    
    # 执行 user 下的 UserBuild.py
    cmd_str = "python3 UserBuild.py"
    os.system(cmd_str)

    # 切换到 output 目录下
    cmd_str = os.path.join(ROOT, OUTPUT)
    print('Enter: ' + cmd_str)
    os.chdir(cmd_str)

    # 复制 OUTPUT/user 下的 *.o 到 OUTPUT/app 目录下
    if not os.path.exists(APP_DIR):
        os.mkdir(APP_DIR)
    cmd_str = "cp " + USER_DIR + '/* ' + APP_DIR
    os.system(cmd_str)

    # 切换到 app 目录下
    cmd_str = os.path.join(ROOT, APP_DIR)
    print('Enter: ' + cmd_str)
    os.chdir(cmd_str)
    
    # 执行 app 下的 AppBuild.py
    cmd_str = "python3 AppBuild.py"
    os.system(cmd_str)

    # 切换到 OUTPUT/app 目录下
    cmd_str = os.path.join(ROOT, APP_OUT)
    print('Enter: ' + cmd_str)
    os.chdir(cmd_str)

    # 复制 app 下的 app.bin 到 OUTPUT 目录下
    cmd_str = "cp app.bin ../"
    os.system(cmd_str)

    # 切回 OUTPUT
    cmd_str = os.path.join(ROOT, OUTPUT)
    print('Enter: ' + cmd_str)
    os.chdir(cmd_str)


# 打印 project
def print_project(project):
    for item in project:
        str_print = item + ': ' + str(project[item])
        print(str_print)

def main():
    # 路径为空即表示当前所在目录
    root_path = ''
    project = Parse_BUILD_CFG(root_path)
    
    # 打印 project
    # print_project(project)

    # 创建文件夹 output
    if not os.path.exists(OUTPUT):
        os.mkdir(OUTPUT)

    # 编译 boot
    compile_boot(BOOT_SRC, BOOT_BIN)

    # 编译 loader
    compile_loader(LOADER_SRC, LOADER_BIN)

    # 编译 kernel
    compile_kernel(project)

    # 切换到 OUTPUT 目录下
    cmd_str = os.path.join(ROOT, OUTPUT)
    print('Enter: ' + cmd_str)
    os.chdir(cmd_str)

    # 链接 kernel obj 临时文件
    ld_kernel_obj(KERNEL_OUT)

    # 提取 elf 程序中的 bin 有效数据
    elf_2_bin(KERNEL_OUT, KERNEL_BIN)

    # 处理 app
    handle_app()

    # 生成最终系统镜像
    write_img(BOOT_BIN, LOADER_BIN, KERNEL_BIN, APP_BIN)

    print("Success!")


if __name__ == '__main__':
    main()